5.05. Коллекции
Коллекции
В реальной разработке редко приходится работать с одним объектом. Чаще всего — у нас есть список пользователей, набор заказов, список настроек, очередь задач и т.д. Чтобы эффективно хранить, обрабатывать и управлять такими группами, в C# существует мощная система коллекций.
Коллекция — это структура данных, предназначенная для хранения набора элементов одного или разных типов. Она позволяет добавлять и удалять элементы, перебирать элементы, искать, сортировать, фильтровать, организовывать данные по определённому принципу (по индексу, по ключу, по приоритету и т.д.).
class User
{
public int Id { get; set; }
public string Name { get; set; }
}
// Создаём коллекцию пользователей
var users = new List<User>
{
new User { Id = 1, Name = "Alice" },
new User { Id = 2, Name = "Bob" },
new User { Id = 3, Name = "Charlie" }
};
// Обрабатываем всех пользователей
foreach (var user in users)
{
Console.WriteLine($"Hello, {user.Name}!");
}
Без коллекций пришлось бы писать user1, user2, user3… — и быстро запутаться. А что если количество заранее неизвестно, и понадобится делать запрос в базу данных - и там может быть как один user, так и тысяча?
Собственно, поэтому коллекции являются основой для такой работы. И они бывают разные, в зависимости от необходимости.
- Хранить много объектов -
List<User>; - Быстро находить по ID -
Dictionary<int, User>; - Избежать дубликатов -
HashSet<User>; - Обрабатывать по порядку (FIFO) -
Queue<Task>; - Обрабатывать «последний — первый» (LIFO) -
Stack<UndoAction>; - Группировать данные -
ILookup<Department, Employee>.
В .NET все коллекции строятся на основе иерархии интерфейсов. Это позволяет писать гибкий, расширяемый и тестируемый код.
| Интерфейс | Назначение |
|---|---|
IEnumerable<T> | Поддержка перебора элементов (например, с помощью foreach) |
ICollection<T> | Предоставляет возможности для добавления, удаления и подсчёта элементов |
IList<T> | Обеспечивает доступ к элементам по индексу (list[0]) и поддержку упорядоченного списка |
IDictionary<TKey, TValue> | Хранение и управление коллекцией пар «ключ-значение» |
ISet<T> | Представляет математическое множество — коллекцию без дублирующихся элементов |
IReadOnlyCollection<T> | Коллекция только для чтения, с возможностью получения количества элементов |
IReadOnlyList<T> | Список только для чтения, с доступом к элементам по индексу |
IEnumerable<T> - лучше всего подходит для переборов. Позволяет использовать foreach, ковариантен (out T) - можно присвоить IEnumerable<string> переменной IEnumerable<object>. Реализуется всеми коллекциями.
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
Пример:
IEnumerable<string> names = new List<string> { "Alice", "Bob" };
foreach (string name in names) { ... }
yield return — ключевое слово для ленивого перебора:
public static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
IEnumerator<T> - это итератор. Он управляет текущим элементом при переборе.
public interface IEnumerator<out T> : IDisposable
{
T Current { get; }
bool MoveNext(); // Переходит к следующему элементу
void Reset(); // Сбрасывает (редко используется)
}
foreach под капотом использует GetEnumerator() и MoveNext().
ICollection<T> используется для изменяемых коллекций. Он наследует IEnumerable<T> и добавляет операции Add, Remove, Count.
public interface ICollection<T> : IEnumerable<T>
{
int Count { get; }
bool IsReadOnly { get; }
void Add(T item);
bool Remove(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int arrayIndex);
}
IList<T> используется для доступа по индексу. Позволяет list[0] = item; и реализуется List<T>, T[].
public interface IList<T> : ICollection<T>
{
T this[int index] { get; set; } // Индексатор
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
}
IDictionary<TKey, TValue> - это словарь. Он хранит пары «ключ - значение» и позволяет получать быстрый доступ по ключу (O(1) в среднем).
public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>
{
TValue this[TKey key] { get; set; }
ICollection<TKey> Keys { get; }
ICollection<TValue> Values { get; }
bool ContainsKey(TKey key);
bool TryGetValue(TKey key, out TValue value);
void Add(TKey key, TValue value);
bool Remove(TKey key);
}
ISet<T> используется как множество без дубликатов, гарантируя уникальность элементов. Реализуется HashSet<T>, SortedSet<T>.
public interface ISet<T>
{
bool Add(T item); // Возвращает false, если уже есть
void UnionWith(IEnumerable<T> other);
void IntersectWith(IEnumerable<T> other);
bool IsSubsetOf(IEnumerable<T> other);
}
IReadOnlyCollection<T> и IReadOnlyList<T> нужны для защиты от изменений. Они используются, когда нужно только читать данные.
public interface IReadOnlyCollection<out T> : IEnumerable<T>
{
int Count { get; }
}
public interface IReadOnlyList<out T> : IReadOnlyCollection<T>
{
T this[int index] { get; }
}
Есть обобщённые и необобщённые коллекции. Необобщённые нетипобезопасны, требуют приведения типов и заметно медленнее из-за распаковки/упаковки. Необобщённые находятся в System.Collections.
Необобщённые (устаревшие):
- ArrayList - Список объектов object;
- Hashtable - Словарь объектов;
- Queue – Очередь;
- Stack – Стек;
- SortedList - Отсортированный список
Обобщённые:
List<T>- упорядоченная коллекция элементов;Dictionary<TKey, TValue>- словарь «ключ-значение»;HashSet<T>- множество без дубликатов;LinkedList<T>- связный список;Queue<T>- FIFO (первый вошел – первый вышел);Stack<T>- LIFO (последний вошел – первый вышел);SortedSet<T>- отсортированное множество;ConcurrentBag<T>- Параллельная коллекция.
Параллельные (потокобезопасные) коллекции (из System.Collections.Concurrent):
ConcurrentQueue<T>ConcurrentStack<T>ConcurrentDictionary<TKey, TValue>
Потокобезопасные (параллельные) коллекции — это специальные структуры данных, разработанные для безопасной работы из нескольких потоков одновременно. Обычные коллекции, такие как List<T>, Dictionary<TKey, TValue>, не являются потокобезопасными. Если к ним обращаются несколько потоков (особенно с записью), возможны повреждения структуры данных, исключения, гонки данных. Чтобы этого избежать, можно использовать блокировки (lock), но это медленно, сложно в отладке и может привести к взаимоблокировкам (deadlock).
Потокобезопасная коллекция позволяет получать одновременный доступ из нескольких потоков, гарантирует целостность данных, обеспечивает атомарность операций и не требует ручных блокировок в большинстве случаев. Они используют низкоуровневые примитивы (например, Interlocked, SpinWait) для эффективной синхронизации.
Если у вас один поток - используйте обычные коллекции, они быстрее. Также не стоит использовать параллельные коллекции, когда нужен порядок и сортировка, ведь они не гарантируют порядок.